Skip to content

chore: Introduce client cache support and add to signers#729

Merged
zeljkoX merged 4 commits into
mainfrom
client-cache
Mar 31, 2026
Merged

chore: Introduce client cache support and add to signers#729
zeljkoX merged 4 commits into
mainfrom
client-cache

Conversation

@zeljkoX
Copy link
Copy Markdown
Collaborator

@zeljkoX zeljkoX commented Mar 27, 2026

Summary

  • Add AsyncClientCache and SyncClientCache primitives for exactly-once client initialization per key
  • Cache AWS KMS SDK client by region (shared across all signers in the same region)
  • Cache Stellar (soroban_rs::Client) and Solana (RpcClient) clients by URL to avoid recreating on every retry
  • Share a single reqwest::Client across Stellar raw HTTP providers; EVM builds per-call clients with baked-in timeout via build_rpc_http_client_with_timeout() (alloy transport requires client-level timeout)
  • Extract base_rpc_client_builder() to centralize reqwest pool/TLS/keepalive settings
  • Cache Stellar signature hints via OnceCell and extract shared derive_signature_hint helper
  • Wrap AWS Client::new() in catch_unwind to convert TLS panics to typed errors in containerized environments

Test plan

  • cargo test --lib client_cache — 11 tests covering async/sync cache semantics, concurrency, error recovery
  • cargo test --lib aws_kms — 45 tests including KMS client cache reuse verification
  • cargo test --lib services::provider::stellar — 47 tests
  • cargo test --lib services::provider::solana — 34 tests
  • cargo test --lib services::provider::evm — 15 tests
  • Manual: verified via production logs that shared RPC client is created once and reused across Stellar transactions

🤖 Generated with Claude Code

@zeljkoX zeljkoX requested a review from a team as a code owner March 27, 2026 08:21
@zeljkoX zeljkoX requested a review from Copilot March 27, 2026 08:21
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Walkthrough

A new global client caching infrastructure is introduced via AsyncClientCache and SyncClientCache utilities. AWS KMS clients are pooled per region with Arc-backed caching and panic handling. RPC HTTP clients are standardized and shared across providers. Stellar and Solana RPC clients are cached per configuration. Signature hint computation is moved to one-time cached initialization in Stellar signers.

Changes

Cohort / File(s) Summary
Client Caching Infrastructure
src/services/client_cache.rs, src/services/mod.rs
New generic AsyncClientCache and SyncClientCache types providing thread-safe exactly-once async/sync initialization with error non-caching semantics, keyed by generic K and storing Arc<V>. Includes test helpers for eviction and length inspection.
AWS KMS Client Pooling
src/services/aws_kms/mod.rs
Global async client cache (KMS_CLIENT_CACHE) keyed by region. AwsKmsClient now holds Arc<Client> instead of plain Client. Client creation wrapped in catch_unwind to convert TLS/config panics to AwsKmsError::ConfigError. Helper functions resolve_aws_region and get_or_create_kms_client manage region resolution and shared client retrieval. Removed redundant drop(cache_write) calls.
RPC HTTP Client Standardization
src/services/provider/mod.rs, src/services/provider/evm/mod.rs
New base_rpc_client_builder() applies consistent pooling, keepalive, and Rustls TLS settings. Global shared RPC HTTP client (SHARED_RPC_HTTP_CLIENT) via get_shared_rpc_http_client(). New build_rpc_http_client_with_timeout(timeout) for custom per-request timeouts. EVM provider now delegates HTTP client construction to the shared builder.
Provider-Specific Client Caching
src/services/provider/solana/mod.rs, src/services/provider/stellar/mod.rs
Solana and Stellar RPC clients are cached per configuration key using SyncClientCache. Solana caches by (URL, timeout, commitment); Stellar caches by URL. Initialization now reuses cached RpcClient/Client instances instead of constructing per call. Removed per-provider hardcoded HTTP client config.
Signature Hint Caching
src/services/signer/stellar/aws_kms_signer.rs, src/services/signer/stellar/google_cloud_kms_signer.rs, src/services/signer/stellar/mod.rs
Moved signature hint derivation from per-call inline computation to one-time cached initialization via tokio::sync::OnceCell. New helper derive_signature_hint(address) centralizes Ed25519 public key parsing and last-4-byte hint extraction. Both AWS KMS and Google Cloud KMS signers initialize cached_hint field at construction. Reduced redundant address parsing and public key operations.

Sequence Diagrams

sequenceDiagram
    participant Caller
    participant AsyncClientCache
    participant OnceCell
    participant InitFn as Init Closure
    
    rect rgba(100, 150, 200, 0.5)
    Note over Caller,InitFn: First concurrent caller for a key
    Caller->>AsyncClientCache: get_or_try_init(key, init_fn)
    AsyncClientCache->>AsyncClientCache: lookup key in DashMap
    AsyncClientCache->>OnceCell: get_or_try_init (empty cell)
    OnceCell->>InitFn: run init closure
    InitFn-->>OnceCell: Arc<V> or Error
    OnceCell-->>AsyncClientCache: result
    AsyncClientCache-->>Caller: Arc<V>
    end
    
    rect rgba(150, 200, 100, 0.5)
    Note over Caller,InitFn: Concurrent caller for same key
    Caller->>AsyncClientCache: get_or_try_init(key, init_fn)
    AsyncClientCache->>AsyncClientCache: lookup key in DashMap
    AsyncClientCache->>OnceCell: await existing cell
    OnceCell-->>AsyncClientCache: cached Arc<V>
    AsyncClientCache-->>Caller: Arc<V>
    end
Loading
sequenceDiagram
    participant Signer as Stellar Signer
    participant OnceCell
    participant KmsService as KMS Service
    participant DeriveHelper as derive_signature_hint()
    
    rect rgba(100, 150, 200, 0.5)
    Note over Signer,DeriveHelper: First call to get_signature_hint
    Signer->>OnceCell: get_or_try_init
    OnceCell->>KmsService: get_stellar_address()
    KmsService-->>OnceCell: Address
    OnceCell->>DeriveHelper: derive_signature_hint(address)
    DeriveHelper->>DeriveHelper: parse Stellar address to Ed25519 pk
    DeriveHelper->>DeriveHelper: extract last 4 bytes
    DeriveHelper-->>OnceCell: SignatureHint
    OnceCell-->>Signer: SignatureHint (cached)
    end
    
    rect rgba(150, 200, 100, 0.5)
    Note over Signer,DeriveHelper: Subsequent calls
    Signer->>OnceCell: get_or_try_init (already initialized)
    OnceCell-->>Signer: cached SignatureHint (no recomputation)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

cla: allowlist

Suggested reviewers

  • tirumerla
  • LuisUrrutia

Poem

🐰 A cache for every client, neat and tidy,
Arc-wrapped treasures pooled on every side-y!
Hints now cached, not freshly made each day,
One-time computations lighten the way! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: Introduce client cache support and add to signers' is concise and clearly summarizes the main change of introducing client caching infrastructure and integrating it into the signers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed PR description is well-structured with a clear summary of changes, comprehensive test plan with specific test commands, and concrete evidence of testing completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch client-cache

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces reusable client-caching primitives and applies them across signers/providers to avoid recreating SDK/RPC/HTTP clients and to cache deterministic Stellar signature hints.

Changes:

  • Added AsyncClientCache / SyncClientCache to guarantee exactly-once initialization per key.
  • Cached AWS KMS SDK clients by region and cached Stellar/Solana RPC clients by URL (and config).
  • Centralized RPC reqwest::Client construction (shared base settings + shared/no-timeout client for Stellar raw HTTP) and cached Stellar signature hints via OnceCell.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/services/signer/stellar/mod.rs Adds shared derive_signature_hint helper for reuse by Stellar signers.
src/services/signer/stellar/google_cloud_kms_signer.rs Caches Stellar signature hint via tokio::sync::OnceCell.
src/services/signer/stellar/aws_kms_signer.rs Caches Stellar signature hint via tokio::sync::OnceCell.
src/services/provider/stellar/mod.rs Caches Soroban RPC clients by URL and switches to shared reqwest::Client for raw HTTP calls.
src/services/provider/solana/mod.rs Caches Solana RpcClient instances keyed by URL/timeout/commitment.
src/services/provider/mod.rs Introduces shared/base reqwest::Client builders for RPC usage.
src/services/provider/evm/mod.rs Uses centralized RPC HTTP client builder with timeout (instead of inline builder).
src/services/mod.rs Exposes new client_cache module within the crate.
src/services/client_cache.rs New cache primitives with unit tests.
src/services/aws_kms/mod.rs Caches AWS KMS SDK client by region and wraps Client::new() in catch_unwind.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/services/provider/evm/mod.rs
Comment thread src/services/provider/mod.rs Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 88.70588% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.26%. Comparing base (672ee6d) to head (377d5e6).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
src/services/aws_kms/mod.rs 87.50% 10 Missing ⚠️
src/services/client_cache.rs 95.57% 10 Missing ⚠️
src/services/signer/stellar/mod.rs 43.75% 9 Missing ⚠️
src/services/signer/stellar/aws_kms_signer.rs 68.18% 7 Missing ⚠️
src/services/provider/stellar/mod.rs 66.66% 5 Missing ⚠️
src/services/provider/mod.rs 91.42% 3 Missing ⚠️
src/services/provider/solana/mod.rs 88.23% 2 Missing ⚠️
src/services/provider/evm/mod.rs 66.66% 1 Missing ⚠️
...services/signer/stellar/google_cloud_kms_signer.rs 90.90% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
properties 0.01% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

@@            Coverage Diff             @@
##             main     #729      +/-   ##
==========================================
+ Coverage   90.20%   90.26%   +0.05%     
==========================================
  Files         289      290       +1     
  Lines      120730   121553     +823     
==========================================
+ Hits       108905   109719     +814     
- Misses      11825    11834       +9     
Files with missing lines Coverage Δ
src/services/provider/evm/mod.rs 81.91% <66.66%> (-0.45%) ⬇️
...services/signer/stellar/google_cloud_kms_signer.rs 91.42% <90.90%> (+2.57%) ⬆️
src/services/provider/solana/mod.rs 85.78% <88.23%> (+0.13%) ⬆️
src/services/provider/mod.rs 92.96% <91.42%> (-0.08%) ⬇️
src/services/provider/stellar/mod.rs 90.26% <66.66%> (+0.18%) ⬆️
src/services/signer/stellar/aws_kms_signer.rs 90.14% <68.18%> (+3.11%) ⬆️
src/services/signer/stellar/mod.rs 28.78% <43.75%> (+4.78%) ⬆️
src/services/aws_kms/mod.rs 89.56% <87.50%> (-0.79%) ⬇️
src/services/client_cache.rs 95.57% <95.57%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/services/signer/stellar/google_cloud_kms_signer.rs (1)

215-236: Add a repeated-sign regression test for cached_hint.

The cache only changes behavior on the second call. The current tests still pass if get_signature_hint() stops reusing the first get_stellar_address() result, so a two-call test with expect_get_stellar_address().times(1) would lock the new behavior down.

As per coding guidelines, "Keep tests minimal, deterministic, and fast".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/signer/stellar/google_cloud_kms_signer.rs` around lines 215 -
236, Add a regression test that calls get_signature_hint() twice to verify
caching: mock GoogleCloudKmsStellarService::get_stellar_address (or the test
mock helper using expect_get_stellar_address()) to return the address once and
assert it is invoked exactly once (e.g., .times(1)), then call
signer.get_signature_hint().await twice and assert both results are equal; this
ensures cached_hint is reused and protects the behavior of cached_hint,
get_signature_hint, and the interaction with get_stellar_address.
src/services/signer/stellar/aws_kms_signer.rs (1)

209-223: Add a regression test for the OnceCell hit path.

The current tests only exercise one hint lookup. A focused test in this module can call get_signature_hint() twice and assert expect_get_stellar_address().times(1) so future refactors do not quietly reintroduce the extra KMS round-trip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/signer/stellar/aws_kms_signer.rs` around lines 209 - 223, Add a
regression test in this module that exercises the OnceCell cached path by
calling Signer::get_signature_hint() (or the relevant method that uses
self.cached_hint) twice and verifying the mocked
aws_kms_service.get_stellar_address() was invoked exactly once (use
expect_get_stellar_address().times(1) on the mock). The test should initialize
the signer with a mocked aws_kms_service that returns a known address, call
get_signature_hint() twice, assert both calls return the same hint, and assert
the mock's times(1) expectation to ensure no second KMS round-trip occurs.
src/services/client_cache.rs (1)

31-38: Remove the unused Clone bound from K.

Neither cache clones the key, so K: Clone just narrows the API and excludes otherwise valid key types.

♻️ Proposed API cleanup
-impl<K: Eq + Hash + Clone, V> AsyncClientCache<K, V> {
+impl<K: Eq + Hash, V> AsyncClientCache<K, V> {
@@
-impl<K: Eq + Hash + Clone, V> Default for AsyncClientCache<K, V> {
+impl<K: Eq + Hash, V> Default for AsyncClientCache<K, V> {
@@
-impl<K: Eq + Hash + Clone, V> SyncClientCache<K, V> {
+impl<K: Eq + Hash, V> SyncClientCache<K, V> {
@@
-impl<K: Eq + Hash + Clone, V> Default for SyncClientCache<K, V> {
+impl<K: Eq + Hash, V> Default for SyncClientCache<K, V> {

Also applies to: 67-71, 84-91, 122-126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/client_cache.rs` around lines 31 - 38, The impl for
AsyncClientCache unnecessarily requires K: Clone—remove the Clone bound from the
impl generics (change impl<K: Eq + Hash + Clone, V> AsyncClientCache<K, V> to
impl<K: Eq + Hash, V> AsyncClientCache<K, V>) and likewise remove Clone from any
other AsyncClientCache impl blocks and method signatures (e.g., the impls that
include get_or_try_init and related methods referenced in the comment) so keys
are not constrained to Clone when not cloned; ensure only Eq + Hash are required
in all AsyncClientCache impl blocks and method headers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/aws_kms/mod.rs`:
- Around line 931-944: The test test_kms_client_creation_returns_error_not_panic
currently sets AwsKmsSignerConfig.region to None which triggers
resolve_aws_region() and yields AwsKmsError::ConfigError before reaching
Client::new()/catch_unwind; change the test to provide a concrete region (e.g.,
Some("us-west-2".to_string())) on AwsKmsSignerConfig so resolve_aws_region is
bypassed, rename the test to reflect it verifies panic-to-error conversion, call
get_or_create_kms_client(&config).await, and tighten the assertion to
assert!(matches!(result, Err(e) if !matches!(e, AwsKmsError::ConfigError(_))))
or assert the specific error variant produced by the
catch_unwind/panic-conversion branch to ensure the new path is exercised.

In `@src/services/client_cache.rs`:
- Line 10: Update the public docs in src/services/client_cache.rs (e.g., the
module-level comment and the doc comments for the ClientCache type and its
get_or_try_init() method) to reflect the actual contract: there is at most one
in-flight initializer per key under concurrent access, but initializers that
return Err are retried on subsequent calls (so initialization is not strictly
"exactly once" on failures). Add or replace the existing non-document-comments
with /// doc comments on all public items referenced in the review (module,
ClientCache, get_or_try_init(), and any other public functions around lines
noted) to state this behavior clearly and precisely.

In `@src/services/provider/solana/mod.rs`:
- Around line 63-64: The cache used for RPC clients (SOLANA_RPC_CLIENT_CACHE /
STELLAR_RPC_CLIENT_CACHE built from SyncClientCache) blocks the Tokio executor
because initialize_provider() is called from async retry_rpc_call() via
get_provider() -> provider_initializer; replace the blocking SyncClientCache
with an async-aware solution (e.g., use a tokio::sync::RwLock-wrapped map or an
async cache crate) so get_or_try_init-style initialization does not take a
std::sync::Mutex in async contexts, or alternatively move the blocking
initialization into tokio::task::spawn_blocking() inside initialize_provider()
to ensure RPC client creation does not block the executor. Ensure references to
SyncClientCache, SOLANA_RPC_CLIENT_CACHE, STELLAR_RPC_CLIENT_CACHE,
retry_rpc_call, get_provider, provider_initializer, and initialize_provider are
updated accordingly.

In `@src/services/provider/stellar/mod.rs`:
- Around line 420-424: The error path currently includes the raw RPC URL in the
ProviderError (in the STELLAR_RPC_CLIENT_CACHE.get_or_try_init closure that
calls Client::new and maps errors to ProviderError::NetworkConfiguration);
replace the raw `url` with a redacted form by calling
`normalize_url_for_log(url)` (or a local `let redacted =
normalize_url_for_log(url)` and use `redacted` inside the map_err message) so
sensitive userinfo/query tokens are not leaked in logs/telemetry.

---

Nitpick comments:
In `@src/services/client_cache.rs`:
- Around line 31-38: The impl for AsyncClientCache unnecessarily requires K:
Clone—remove the Clone bound from the impl generics (change impl<K: Eq + Hash +
Clone, V> AsyncClientCache<K, V> to impl<K: Eq + Hash, V> AsyncClientCache<K,
V>) and likewise remove Clone from any other AsyncClientCache impl blocks and
method signatures (e.g., the impls that include get_or_try_init and related
methods referenced in the comment) so keys are not constrained to Clone when not
cloned; ensure only Eq + Hash are required in all AsyncClientCache impl blocks
and method headers.

In `@src/services/signer/stellar/aws_kms_signer.rs`:
- Around line 209-223: Add a regression test in this module that exercises the
OnceCell cached path by calling Signer::get_signature_hint() (or the relevant
method that uses self.cached_hint) twice and verifying the mocked
aws_kms_service.get_stellar_address() was invoked exactly once (use
expect_get_stellar_address().times(1) on the mock). The test should initialize
the signer with a mocked aws_kms_service that returns a known address, call
get_signature_hint() twice, assert both calls return the same hint, and assert
the mock's times(1) expectation to ensure no second KMS round-trip occurs.

In `@src/services/signer/stellar/google_cloud_kms_signer.rs`:
- Around line 215-236: Add a regression test that calls get_signature_hint()
twice to verify caching: mock GoogleCloudKmsStellarService::get_stellar_address
(or the test mock helper using expect_get_stellar_address()) to return the
address once and assert it is invoked exactly once (e.g., .times(1)), then call
signer.get_signature_hint().await twice and assert both results are equal; this
ensures cached_hint is reused and protects the behavior of cached_hint,
get_signature_hint, and the interaction with get_stellar_address.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ecabf19-54bf-416d-aa99-261bb0b7849c

📥 Commits

Reviewing files that changed from the base of the PR and between d5c5f13 and 7784218.

📒 Files selected for processing (10)
  • src/services/aws_kms/mod.rs
  • src/services/client_cache.rs
  • src/services/mod.rs
  • src/services/provider/evm/mod.rs
  • src/services/provider/mod.rs
  • src/services/provider/solana/mod.rs
  • src/services/provider/stellar/mod.rs
  • src/services/signer/stellar/aws_kms_signer.rs
  • src/services/signer/stellar/google_cloud_kms_signer.rs
  • src/services/signer/stellar/mod.rs

Comment thread src/services/aws_kms/mod.rs Outdated
Comment thread src/services/client_cache.rs Outdated
//! - [`SyncClientCache`] — for synchronous client constructors
//! (e.g., `soroban_rs::Client::new(url)`, Solana `RpcClient::new(...)`)
//!
//! Both guarantee exactly-once initialization per key under concurrent access.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the public contract around failed initializers.

Both caches retry after init() returns Err, so the current "exactly once" wording is stronger than the implementation. Please put the actual contract on get_or_try_init() as well: one in-flight initializer per key, with retries after failure.

As per coding guidelines, "Include relevant doc comments (///) on public functions, structs, and modules."

Also applies to: 24-25, 31-54, 77-78, 84-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/client_cache.rs` at line 10, Update the public docs in
src/services/client_cache.rs (e.g., the module-level comment and the doc
comments for the ClientCache type and its get_or_try_init() method) to reflect
the actual contract: there is at most one in-flight initializer per key under
concurrent access, but initializers that return Err are retried on subsequent
calls (so initialization is not strictly "exactly once" on failures). Add or
replace the existing non-document-comments with /// doc comments on all public
items referenced in the review (module, ClientCache, get_or_try_init(), and any
other public functions around lines noted) to state this behavior clearly and
precisely.

Comment thread src/services/provider/solana/mod.rs
Comment thread src/services/provider/stellar/mod.rs
Copy link
Copy Markdown
Contributor

@tirumerla tirumerla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@zeljkoX zeljkoX merged commit aede8aa into main Mar 31, 2026
30 of 32 checks passed
@zeljkoX zeljkoX deleted the client-cache branch March 31, 2026 05:32
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants